/******************************************************************************
 *
 * Copyright (C) 1997-2019 by Dimitri van Heesch.
 *
 * Permission to use, copy, modify, and distribute this software and its
 * documentation under the terms of the GNU General Public License is hereby 
 * granted. No representations are made about the suitability of this software 
 * for any purpose. It is provided "as is" without express or implied warranty.
 * See the GNU General Public License for more details.
 *
 */

%option never-interactive
%option prefix="config_doxywYY"
%top{
#include <stdint.h>
}

%{

/*
 *	includes
 */
#include "config.h"
#include "input.h"
#include "inputstring.h"

#include <QString>
#include <QVariant>
#include <QStack>
#include <QTextCodec>
#include <QByteArray>
#include <QFileInfo>
#include <QStringList>
#include <QRegExp>
#include <QTextStream>
#include <QMessageBox>

#define YY_NO_UNISTD_H 1

#define MAX_INCLUDE_DEPTH 10


/* -----------------------------------------------------------------
 *
 *	static variables
 */

struct ConfigFileState
{
  int lineNr;
  FILE *file;
  YY_BUFFER_STATE oldState;
  YY_BUFFER_STATE newState;
  QString fileName;
};  

static const QHash<QString,Input*>   *g_options;
static FILE                          *g_file;
static QString                        g_yyFileName;
static QString                        g_includeName;
static QVariant                       g_includePathList;
static QStack<ConfigFileState*>       g_includeStack;  
static int                            g_includeDepth;
static QVariant                      *g_arg;
static Input                         *g_curOption=0;
static QString                        g_elemStr;
static QTextCodec                    *g_codec     = QTextCodec::codecForName("UTF-8");
static QString                        g_codecName = QString::fromLatin1("UTF-8");
static int                            g_lastState;
static QByteArray                     g_tmpString;

static const char *stateToString(int state);

/* -----------------------------------------------------------------
 */
#undef	YY_INPUT
#define	YY_INPUT(buf,result,max_size) result=yyread(buf,max_size);

static int yyread(char *buf,int maxSize)
{
    // no file included
    if (g_includeStack.isEmpty()) 
    {
      return fread(buf,1,maxSize,g_file);
    } 
    else 
    {
      return fread(buf,1,maxSize,g_includeStack.top()->file);
    }
}

static QString warning_str = QString::fromLatin1("warning: ");
static QString error_str = QString::fromLatin1("error: ");

void config_err(const char *fmt, ...)
{
  QString msg = error_str;
  msg.append(QString::fromLatin1(fmt));
  va_list args;
  va_start(args, fmt);
  vfprintf(stderr, qPrintable(msg), args);
  va_end(args);
}
void config_warn(const char *fmt, ...)
{
  QString msg = warning_str;
  msg.append(QString::fromLatin1(fmt));
  va_list args;
  va_start(args, fmt);
  vfprintf(stderr, qPrintable(msg), args);
  va_end(args);
}

static void substEnvVarsInStrList(QStringList &sl);
static void substEnvVarsInString(QString &s);

static void checkEncoding()
{
  Input *option = g_options->value(QString::fromLatin1("DOXYFILE_ENCODING"));
  if (option && option->value().toString()!=g_codecName)
  {
    QTextCodec *newCodec = QTextCodec::codecForName(option->value().toString().toLatin1());
    if (newCodec)
    {
      g_codec = newCodec;
      g_codecName = option->value().toString();
    }
  }
}

static FILE *tryPath(const QString &path,const QString &fileName)
{
  QString absName=!path.isEmpty() ? path+QString::fromLatin1("/")+fileName : fileName;
  QFileInfo fi(absName);
  if (fi.exists() && fi.isFile())
  {
    FILE *f = fopen(absName.toLocal8Bit(),"r");
    if (f==NULL)
      config_err("could not open file %s for reading\n",qPrintable(absName));
    else 
      return f;
  }
  return NULL;
}

static FILE *findFile(const QString &fileName)
{
  if (QFileInfo(fileName).isAbsolute()) // absolute path
  {
    return tryPath(QString(), fileName);
  }

  // relative path, try with include paths in the list
  QStringList sl = g_includePathList.toStringList();
  substEnvVarsInStrList(sl);
  foreach (QString s, sl) 
  {
    FILE *f = tryPath(s,fileName);
    if (f) return f;
  }
  // try cwd if g_includePathList fails
  return tryPath(QString::fromLatin1("."),fileName);
}

static void readIncludeFile(const QString &incName)
{
  if (g_includeDepth==MAX_INCLUDE_DEPTH) 
  {
    config_err("maximum include depth (%d) reached, %s is not included. Aborting...\n",
	MAX_INCLUDE_DEPTH,qPrintable(incName));
    exit(1);
  } 

  QString inc = incName;
  substEnvVarsInString(inc);
  inc = inc.trimmed();
  uint incLen = inc.length();
  if (inc.at(0)==QChar::fromLatin1('"') && 
      inc.at(incLen-1)==QChar::fromLatin1('"')) // strip quotes
  {
    inc=inc.mid(1,incLen-2);
  }

  FILE *f = findFile(inc);
  if (f) // see if the include file can be found
  {
    // For debugging
#if SHOW_INCLUDES
    for (i=0;i<includeStack.count();i++) msg("  ");
    msg("@INCLUDE = %s: parsing...\n",qPrintable(inc));
#endif

    // store the state of the old file 
    ConfigFileState *fs=new ConfigFileState;
    fs->oldState=YY_CURRENT_BUFFER;
    fs->fileName=g_yyFileName;
    fs->file=f;
    // push the state on the stack
    g_includeStack.push(fs);
    // set the scanner to the include file
    yy_switch_to_buffer(yy_create_buffer(f, YY_BUF_SIZE));
    fs->newState=YY_CURRENT_BUFFER;
    g_yyFileName=inc;
    g_includeDepth++;
  } 
  else
  {
    config_err("@INCLUDE = %s: not found!\n",qPrintable(inc));
    exit(1);
  }
}


%}

%option nounput
%option noyywrap
%option yylineno

%x      Start
%x	SkipComment
%x      SkipInvalid
%x      GetString
%x      GetEnum
%x      GetStrList
%x      GetQuotedString
%x      GetEnvVar
%x      Include

%%

<*>\0x0d
<Start,GetString,GetEnum,GetStrList,SkipInvalid>"#"	 { BEGIN(SkipComment); }
<Start>[a-z_A-Z][a-z_A-Z0-9]*[ \t]*"="	 { QString cmd = g_codec->toUnicode(yytext);
                                           cmd=cmd.left(cmd.length()-1).trimmed(); 
					   g_curOption = g_options->value(cmd);
					   if (g_curOption==0) // oops not known
					   {
					     config_warn("ignoring unsupported tag '%s' at line %d, file %s\n",
						 qPrintable(cmd),yylineno,qPrintable(g_yyFileName)); 
					     BEGIN(SkipInvalid);
					   }
					   else // known tag
					   {
					     //option->setEncoding(encoding);
					     g_arg = &g_curOption->value();
					     switch(g_curOption->kind())
					     {
					       case Input::StrList:
						 g_elemStr = QString();
						 *g_arg = QStringList();
					         BEGIN(GetStrList);
					         break;
					       case Input::String:
                                                 if (dynamic_cast<InputString *>(g_curOption)->stringMode() == InputString::StringFixed)
                                                 {
                                                   BEGIN(GetEnum);
                                                 }
                                                 else
                                                 {
                                                   BEGIN(GetString);
                                                 }
					         break;
					       case Input::Int:
					         BEGIN(GetString);
					         break;
					       case Input::Bool:
					         BEGIN(GetString);
						 break;
					       case Input::Obsolete:
					         config_warn("Tag '%s' at line %d of file %s has become obsolete.\n"
						            "To avoid this warning please update your configuration "
							    "file using \"doxygen -u\"\n", qPrintable(cmd),
							    yylineno,qPrintable(g_yyFileName)); 
					         BEGIN(SkipInvalid);
						 break;
					     }
					   }
					}
<Start>[a-z_A-Z][a-z_A-Z0-9]*[ \t]*"+="	{ QString cmd=g_codec->toUnicode(yytext);
                                          cmd=cmd.left(cmd.length()-2).trimmed(); 
					  g_curOption = g_options->value(cmd);
					  if (g_curOption==0) // oops not known
					  {
					    config_warn("ignoring unsupported tag '%s' at line %d, file %s\n",
						yytext,yylineno,qPrintable(g_yyFileName)); 
					    BEGIN(SkipInvalid);
					  }
					  else // known tag
					  {
					    switch(g_curOption->kind())
					    {
					      case Input::StrList:
						g_arg = &g_curOption->value();
						g_elemStr=QString();
					        BEGIN(GetStrList);
					        break;
					      case Input::String:
					      case Input::Int:
					      case Input::Bool:
					        config_warn("operator += not supported for '%s'. Ignoring line at line %d, file %s\n",
						    yytext,yylineno,qPrintable(g_yyFileName)); 
					        BEGIN(SkipInvalid);
						break;
					      case Input::Obsolete:
					         config_warn("Tag '%s' at line %d of file %s has become obsolete.\n"
						            "To avoid this warning please update your configuration "
							    "file using \"doxygen -u\"\n", 
							    qPrintable(cmd),yylineno,qPrintable(g_yyFileName)); 
					         BEGIN(SkipInvalid);
						 break;
					     }
					   }
					}
<Start>"@INCLUDE_PATH"[ \t]*"=" 	{ BEGIN(GetStrList); g_arg=&g_includePathList; *g_arg = QStringList(); g_elemStr=QString(); }
  /* include a config file */
<Start>"@INCLUDE"[ \t]*"="     		{ BEGIN(Include);}
<Include>([^ \"\t\r\n]+)|("\""[^\n\"]+"\"") { 
  					  readIncludeFile(g_codec->toUnicode(yytext)); 
  					  BEGIN(Start);
					}
<<EOF>>					{
                                          //printf("End of include file\n");
					  //printf("Include stack depth=%d\n",g_includeStack.count());
                                          if (g_includeStack.isEmpty())
					  {
					    //printf("Terminating scanner!\n");
					    yyterminate();
					  }
					  else
					  {
					    ConfigFileState *fs = g_includeStack.pop();
					    fclose(fs->file);
					    YY_BUFFER_STATE oldBuf = YY_CURRENT_BUFFER;
					    yy_switch_to_buffer( fs->oldState );
					    yy_delete_buffer( oldBuf );
					    g_yyFileName=fs->fileName;
					    delete fs; 
                                            g_includeDepth--;
					  }
  					}

<Start>[a-z_A-Z0-9]+			{ config_warn("ignoring unknown tag '%s' at line %d, file %s\n",yytext,yylineno,qPrintable(g_yyFileName)); }
<GetString,GetEnum,SkipInvalid>\n	        { BEGIN(Start); }
<GetStrList>\n				{ 
					  if (!g_elemStr.isEmpty())
					  {
					    //printf("elemStr1='%s'\n",qPrintable(elemStr));
					    *g_arg = QVariant(g_arg->toStringList() << g_elemStr);
					  }
					  BEGIN(Start); 
					}
<GetStrList>[ \t]+			{
  				          if (!g_elemStr.isEmpty())
					  {
					    //printf("elemStr2='%s'\n",qPrintable(elemStr));
  					    *g_arg = QVariant(g_arg->toStringList() << g_elemStr);
					  }
					  g_elemStr = QString();
  					}
<GetString>[^ \"\t\r\n]+		{
                                          *g_arg = QVariant(g_codec->toUnicode(yytext)); 
                                          checkEncoding();
                                        }
<GetEnum>[^ \"\t\r\n]+			{
                                          InputString *cur = dynamic_cast<InputString *>(g_curOption);
                                          *g_arg = cur->checkEnumVal(g_codec->toUnicode(yytext));
                                        }
<GetString,GetEnum,GetStrList,SkipInvalid>"\""	{ g_lastState=YY_START;
  					  BEGIN(GetQuotedString); 
                                          g_tmpString="";
					}
<GetQuotedString>"\""|"\n" 		{ 
                                          // we add a bogus space to signal that the string was quoted. This space will be stripped later on.
                                          g_tmpString+=" ";
  					  //printf("Quoted String = '%s'\n",qPrintable(tmpString));
					  if (g_lastState==GetString)
					  {
					    *g_arg = g_codec->toUnicode(g_tmpString);
                                            checkEncoding();
					  }
					  else if (g_lastState==GetEnum)
					  {
                                            InputString *cur = dynamic_cast<InputString *>(g_curOption);
                                            *g_arg = cur->checkEnumVal(g_codec->toUnicode(g_tmpString));
					  }
					  else
					  {
					    g_elemStr+=g_codec->toUnicode(g_tmpString);
					  }
					  if (*yytext=='\n')
					  {
					    config_warn("Missing end quote (\") on line %d, file %s\n",yylineno,
                                                qPrintable(g_yyFileName));
					  }
					  BEGIN(g_lastState);
  					}
<GetQuotedString>"\\\""			{
  					  g_tmpString+='"';
  					}
<GetQuotedString>.			{ g_tmpString+=*yytext; }
<GetStrList>[^ \#\"\t\r\n]+		{
  					  g_elemStr+=g_codec->toUnicode(yytext);
  					}
<SkipComment>\n				{ BEGIN(Start); }
<SkipComment>\\[ \r\t]*\n		{ BEGIN(Start); }
<*>\\[ \r\t]*\n				{ }
<*>\n
<*>.					

%%

/*@ ----------------------------------------------------------------------------
 */

static void substEnvVarsInString(QString &s)
{
  static QRegExp re(QString::fromLatin1("\\$\\([a-z_A-Z0-9]+\\)"));
  if (s.isEmpty()) return;
  int p=0;
  int i,l;
  //printf("substEnvVarInString(%s) start\n",qPrintable(s));
  while ((i=re.indexIn(s,p))!=-1)
  {
    l = re.matchedLength();
    //printf("Found environment var s.mid(%d,%d)='%s'\n",i+2,l-3,qPrintable(s.mid(i+2,l-3)));
    QString env=g_codec->toUnicode(getenv(s.mid(i+2,l-3).toLatin1()));
    substEnvVarsInString(env); // recursively expand variables if needed.
    s = s.left(i)+env+s.right(s.length()-i-l);
    p=i+env.length(); // next time start at the end of the expanded string
  }
  s=s.trimmed(); // to strip the bogus space that was added when an argument
                         // has quotes
  //printf("substEnvVarInString(%s) end\n",qPrintable(s));
}

static void substEnvVarsInStrList(QStringList &sl)
{
  QStringList out;

  foreach (QString result, sl)
  {
    // an argument with quotes will have an extra space at the end, so wasQuoted will be TRUE.
    bool wasQuoted = (result.indexOf(QChar::fromLatin1(' '))!=-1) || 
                     (result.indexOf(QChar::fromLatin1('\t'))!=-1);
    // here we strip the quote again
    substEnvVarsInString(result);

    //printf("Result %s was quoted=%d\n",qPrintable(result),wasQuoted);

    if (!wasQuoted) /* as a result of the expansion, a single string
		       may have expanded into a list, which we'll
		       add to sl. If the original string already
		       contained multiple elements no further 
		       splitting is done to allow quoted items with spaces! */
    {
      int l=result.length();
      int i,p=0;
      // skip spaces
      // search for a "word"
      for (i=0;i<l;i++)
      {
	QChar c=0;
	// skip until start of new word
	while (i<l && ((c=result.at(i))==QChar::fromLatin1(' ') || c==QChar::fromLatin1('\t'))) i++; 
	p=i; // p marks the start index of the word
	// skip until end of a word
	while (i<l && ((c=result.at(i))!=QChar::fromLatin1(' ') && 
	              c!=QChar::fromLatin1('\t') && 
		      c!=QChar::fromLatin1('"'))) i++;
	if (i<l) // not at the end of the string
	{
	  if (c==QChar::fromLatin1('"')) // word within quotes
	  {
	    p=i+1;
	    for (i++;i<l;i++)
	    {
	      c=result.at(i);
	      if (c==QChar::fromLatin1('"')) // end quote
	      {
                out += result.mid(p,i-p);
		p=i+1;
		break; 
	      }
	      else if (c==QChar::fromLatin1('\\')) // skip escaped stuff
	      {
		i++;
	      }
	    }
	  }
	  else if (c==QChar::fromLatin1(' ') || c==QChar::fromLatin1('\t')) // separator
	  {
            out += result.mid(p,i-p);
	    p=i+1;
	  }
	}
      }
      if (p!=l) // add the leftover as a string
      {
        out += result.right(l-p);
      }
    }
    else // just goto the next element in the list
    {
      out += result;
    }
  }
  sl = out;
}

//--------------------------------------------------------------------------

bool parseConfig(
      const QString &fileName,
      const QHash<QString,Input *> &options
    )
{
  QHashIterator<QString, Input*> i(options);
  g_file = fopen(fileName.toLocal8Bit(),"r");
  if (g_file==NULL) return false;

  // reset all values
  i.toFront();
  while (i.hasNext()) 
  {
    i.next();
    if (i.value())
    {
      i.value()->reset();
    }
  }

  // parse config file
  g_options       = &options;
  g_yyFileName    = fileName;
  g_includeStack.clear();
  g_includeDepth  = 0;
  config_doxywYYrestart( config_doxywYYin );
  BEGIN( Start );
  config_doxywYYlex();

  // update the values in the UI
  i.toFront();
  while (i.hasNext()) 
  {
    i.next();
    if (i.value())
    {
      //printf("Updating: %s\n",qPrintable(i.key()));
      i.value()->update();
    }
    else
    {
      printf("Invalid option: %s\n",qPrintable(i.key()));
    }
  } 
  fclose(g_file);
  return true;
}

void writeStringValue(QTextStream &t,QTextCodec *codec,const QString &s)
{
  QChar c;
  bool needsEscaping=false;
  bool needsHashEscaping=false;
  // convert the string back to it original encoding
  //QByteArray se = codec->fromUnicode(s);
  t.setCodec(codec);
  const QChar *p=s.data();
  if (!s.isEmpty() && !p->isNull())
  {
    if (*p != QChar::fromLatin1('"'))
    {
      while (!(c=*p++).isNull() && !needsEscaping) 
      {
        needsEscaping = (c==QChar::fromLatin1(' ')  || 
	                 c==QChar::fromLatin1('\n') || 
		         c==QChar::fromLatin1('\t') || 
		         c==QChar::fromLatin1('"'));
      }
      p=s.data();
      while (!(c=*p++).isNull() && !needsHashEscaping) 
      {
        needsHashEscaping = (c==QChar::fromLatin1('#'));
      }
    }
    if (needsHashEscaping || needsEscaping)
    { 
      t << "\"";
    }
    if (needsEscaping)
    { 
      p=s.data();
      while (!p->isNull())
      {
	if (*p   ==QChar::fromLatin1(' ') && 
	   *(p+1)==QChar::fromLatin1('\0')) break; // skip inserted space at the end
	if (*p   ==QChar::fromLatin1('"')) t << "\\"; // escape quotes
	t << *p++;
      }
    }
    else
    {
      t << s;
    }
    if (needsHashEscaping || needsEscaping)
    { 
      t << "\"";
    }
  }
}
#include "config_doxyw.l.h"
